﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Web.Razor;
using System.Web.Razor.Generator;
using Microsoft.CSharp;
using PropertyExpression.Common;
using System.Data;
using Roslyn.Compilers;
using Roslyn.Compilers.Common;
using Roslyn.Compilers.CSharp;

namespace PropertyExpression.ControllableQuery
{
    //TODO: сделать internal
    public static class Util
    {
        //TODO: сделать internal
        public static object ParamInference<T>(T arg)
        {
            return Cache<T>.Func(arg);
        }

        private static class Cache<T>
        {
            internal static readonly Func<T, object> Func = GetFunc();

            private static Func<T, object> GetFunc()
            {
                var type = typeof(T);
                if (DynamicMaterializer.IsAnonymousType(type))
                    return DynamicMaterializer.BuildAnonymousTypeParamInference<T>();
                //TODO: добавить остальные типы
                //TODO: добавить Func<TResult>
                if (type == typeof(int)) return _ => ((int) (object) _).Param();
                if (type == typeof(string)) return _ => ((string)(object)_).Param();
                if (type == typeof(Guid)) return _ => ((Guid)(object)_).Param();
                if (type == typeof(DateTime)) return _ => ((DateTime)(object)_).Param();
                if (type.IsGenericType)
                {
                    var genericTypeDefinition = type.GetGenericTypeDefinition();
                    if (genericTypeDefinition == typeof (Option<>))
                    {
                        var argument = type.GetGenericArguments()[0];
                        return (Func<T, object>) selectMethod.MakeGenericMethod(argument, ParamInferenceType(argument))
                            .CreateDelegate(typeof (Func<T, object>));
                    }
                    if (genericTypeDefinition == typeof(Nullable<>))
                    {
                        var argument = type.GetGenericArguments()[0];
                        return (Func<T, object>)nullableSelectMethod.MakeGenericMethod(argument, ParamInferenceType(argument))
                            .CreateDelegate(typeof(Func<T, object>));
                    }
                }
                return _ => _;
            }
        }

        private static readonly MethodInfo nullableSelectMethod =
            QBuilder.GetMethodInfo<Func<int?, object>>(x1 => Select<int, int>(x1))
                .GetGenericMethodDefinition();

        private static object Select<T, TResult>(T? arg) where T: struct where TResult: struct
        {
            return arg.HasValue ? (TResult) Cache<T>.Func(arg.Value) : new TResult?();
        }

        private static readonly MethodInfo selectMethod =
            QBuilder.GetMethodInfo<Func<Option<object>, object>>(x1 => Select<object, object>(x1))
                .GetGenericMethodDefinition();

        private static object Select<T, TResult>(Option<T> arg)
        {
            return arg.Select(_ => (TResult)Cache<T>.Func(_));
        }

        public static Type ParamInferenceType(Type type)
        {
            if (DynamicMaterializer.IsAnonymousType(type))
            {
                if (!type.IsGenericType) return type;
                return ParamInferenceTypeForAnonymousType(type);
            }
            //TODO: добавить остальные типы
            if (type == typeof (int)) return typeof (Param<int>);
            if (type == typeof(string)) return typeof(Param<string>);
            if (type == typeof(Guid)) return typeof(Param<Guid>);
            if (type == typeof(DateTime)) return typeof(Param<DateTime>);
            if (type.IsGenericType)
            {
                var genericTypeDefinition = type.GetGenericTypeDefinition();
                if (genericTypeDefinition == typeof (Option<>))
                    return typeof (Option<>).MakeGenericType(ParamInferenceType(type.GetGenericArguments()[0]));
                if (genericTypeDefinition == typeof(Nullable<>))
                    return typeof(Nullable<>).MakeGenericType(ParamInferenceType(type.GetGenericArguments()[0]));
            }
            return type;
        }

        internal static Type ParamInferenceTypeForAnonymousType(Type type)
        {
            return type.GetGenericTypeDefinition()
                .MakeGenericType(type.GetGenericArguments().Select(ParamInferenceType).ToArray());
        }

        internal static TResult ParamInferenceInternal<T, TResult>(T arg)
        {
            return (TResult) Cache<T>.Func(arg);
        }

        internal static readonly MethodInfo ParamInferenceMethod =
            QBuilder.GetMethodInfo<Func<object, object>>(x1 => ParamInferenceInternal<object, object>(x1))
                .GetGenericMethodDefinition();

        public static string GetQueryPath(string filePath, int lineNumber)
        {
            var info = new FileInfo(filePath);
            return Path.Combine(info.DirectoryName,
                Path.GetFileNameWithoutExtension(info.Name) + "_" + lineNumber + ".cshtml");
        }

        public const string WriteToMethodName = "PropertyExpression.ControllableQuery.QWriter.WriteTo";
        public const string WriteLiteralToMethodName = "PropertyExpression.ControllableQuery.QWriter.WriteLiteralTo";

        private static readonly RazorCache razorCache = new RazorCache();

        private class RazorCache
        {
            private readonly ConcurrentDictionary<int, Item> dictionary = new ConcurrentDictionary<int, Item>();

            public Option<RazorCacheItem> Get(int lineNumber, string filePath)
            {
                Item value;
                return dictionary.TryGetValue(lineNumber, out value) ? value.Get(filePath) : new Option<RazorCacheItem>();
            }

            public void Set(int lineNumber, string filePath, RazorCacheItem item)
            {
                Item value;
                if (dictionary.TryGetValue(lineNumber, out value))
                    value.Set(lineNumber, filePath, item);
                else
                    dictionary[lineNumber] = new SingleItem(item, filePath, this);
            }

            private abstract class Item
            {
                public abstract Option<RazorCacheItem> Get(string aFilePath);
                public abstract void Set(int lineNumber, string aFilePath, RazorCacheItem aItem);
            }

            private class SingleItem : Item
            {
                private readonly RazorCacheItem item;
                private readonly string filePath;
                private readonly RazorCache parent;

                public SingleItem(RazorCacheItem item, string filePath, RazorCache parent)
                {
                    this.item = item;
                    this.filePath = filePath;
                    this.parent = parent;
                }

                public override Option<RazorCacheItem> Get(string aFilePath)
                {
                    return filePath == aFilePath ? item : new Option<RazorCacheItem>();
                }

                public override void Set(int lineNumber, string aFilePath, RazorCacheItem aItem)
                {
                    parent.dictionary[lineNumber] =
                        new MultipleItem(new ConcurrentDictionary<string, RazorCacheItem>(new[] {
                        new KeyValuePair<string, RazorCacheItem>(filePath, item),
                        new KeyValuePair<string, RazorCacheItem>(aFilePath, aItem)
                    }));
                }
            }

            private class MultipleItem : Item
            {
                private readonly ConcurrentDictionary<string, RazorCacheItem> items;

                public MultipleItem(ConcurrentDictionary<string, RazorCacheItem> items)
                {
                    this.items = items;
                }

                public override Option<RazorCacheItem> Get(string aFilePath)
                {
                    RazorCacheItem value;
                    return items.TryGetValue(aFilePath, out value) ? value : new Option<RazorCacheItem>();
                }

                public override void Set(int lineNumber, string aFilePath, RazorCacheItem aItem)
                {
                    items[aFilePath] = aItem;
                }
            }
        }

        private class RazorCacheItem
        {
            public readonly Func<object, QBuilder, TemplateBase> Func;
            public readonly string Sql;
            public readonly Type ParamType;

            public RazorCacheItem(Func<object, QBuilder, TemplateBase> func, string sql, Type paramType)
            {
                Func = func;
                Sql = sql;
                ParamType = paramType;
            }
        }

        internal static IQuery<Type<T>> Query<T, TParam>(TParam param, string sql, CommandType commandType,
            int lineNumber, string filePath, Option<MethodBase> methodOption = new Option<MethodBase>())
        {
            return GetQBuilder(param, sql, commandType, lineNumber, filePath, methodOption).Query<T>();
        }

        internal static IQuery<T> BatchQuery<T, TParam>(TParam param, string sql, CommandType commandType,
            int lineNumber, string filePath, Option<MethodBase> methodOption = new Option<MethodBase>())
        {
            return GetQBuilder(param, sql, commandType, lineNumber, filePath, methodOption).BatchQuery<T>();
        }

        internal static INonQuery NonQuery<TParam>(TParam param, string sql, CommandType commandType,
            int lineNumber, string filePath, Option<MethodBase> methodOption = new Option<MethodBase>())
        {
            return GetQBuilder(param, sql, commandType, lineNumber, filePath, methodOption).NonQuery();
        }

        internal static QBuilder GetQBuilder<TParam>(TParam param, string sql, CommandType commandType,
            int lineNumber, string filePath, Option<MethodBase> methodOption)
        {
            if (commandType == CommandType.Text)
            {
                var value = razorCache.Get(lineNumber, filePath);
                var paramInference = ParamInference(param);
                var type = paramInference.GetType();
                Func<object, QBuilder, TemplateBase> @delegate;
                if (value.HasValue)
                {
                    var razorCacheItem = value.Value;
                    if (razorCacheItem.Sql != sql) throw new ApplicationException(); //TODO: написать сообщение
                    if (razorCacheItem.ParamType != type) throw new ApplicationException(); //TODO: написать сообщение
                    @delegate = razorCacheItem.Func;
                }
                else
                {
                    //TODO: теперь имя класса может быть постоянным, поскольку эмитем в отдельную сборку. Сделать это.
                    var template = "T" + Guid.NewGuid().ToString("N");
                    var host = new RazorEngineHost(new CSharpRazorCodeLanguage()) {
                        GeneratedClassContext = new GeneratedClassContext(
                            executeMethodName: GeneratedClassContext.Default.ExecuteMethodName,
                            writeMethodName: GeneratedClassContext.Default.WriteMethodName,
                            writeLiteralMethodName: GeneratedClassContext.Default.WriteLiteralMethodName,
                            writeToMethodName: WriteToMethodName,
                            writeLiteralToMethodName: WriteLiteralToMethodName,
                            templateTypeName: typeof (SubQuery).FullName,
                            defineSectionMethodName: GeneratedClassContext.Default.DefineSectionMethodName),
                        DefaultBaseClass = typeof (TemplateBase).FullName,
                        DefaultClassName = template
                    };
                    var method = methodOption.HasValue ? methodOption.Value : new StackTrace().GetFrame(
#if DEBUG
                        3
#else
                        1
#endif
                        ).GetMethod();
                    foreach (var namespaceImport in GetRazorConfiguration(method)
                        .GetNamespaceImports(method))
                        host.NamespaceImports.Add(namespaceImport);

                    var engine = new RazorTemplateEngine(host);
                    GeneratorResults razorResult;
                    using (var reader = new StringReader(sql))
                        razorResult = engine.GenerateCode(reader, className: null, rootNamespace: null,
                            sourceFileName: GetQueryPath(filePath, lineNumber));
                    if (!razorResult.Success)
                    {
                        var error = razorResult.ParserErrors[0];
                        throw new ApplicationException(
                            string.Format("Razor parsing error: '{3}' (Caller line number {4}, Ln {0}, Col {1}) {2}",
                                error.Location.LineIndex + 1, error.Location.CharacterIndex + 1, error.Message,
                                filePath, lineNumber));
                    }
                    StringWriter stringWriter;
                    using (stringWriter = new StringWriter())
                    using (var codeProvider = new CSharpCodeProvider())
                        codeProvider.GenerateCodeFromCompileUnit(
                            razorResult.GeneratedCode, stringWriter, new CodeGeneratorOptions());

                    var propertyInfos = type.GetProperties().Select(_ => new {
                        _.Name,
                        _.PropertyType,
                        SetterName = "." + _.Name
                    }).ToList();
                    propertyInfos.Add(new {
                        Name = "p",
                        PropertyType = type,
                        SetterName = ""
                    });

                    var types = new Dictionary<Assembly, string>();
                    var syntax = GetTypeSyntax(type, types);
                    var typeSyntaxs = propertyInfos.Select(_ => GetTypeSyntax(_.PropertyType, types)).ToList();

                    var compilationUnitSyntax = SyntaxTree.ParseText(stringWriter + @"
namespace Razor
{
" + string.Join(@"
", types.Select(_ => "    extern alias " + _.Value + ";")) + @"

    public partial class " + template + @"
    {
        internal static " + typeof(TemplateBase).FullName + @" M58a3d6d024dc47c6a692e20e29210b89(object arg, " +
                        typeof(QBuilder).FullName + @" builder) {
            var arg2 = (System.Object)arg;
            var result = new " + template + @"(builder);
" + string.Join(@"
", propertyInfos.Select(_ => "            result." + _.Name + " = arg2" + _.SetterName + ";")) + @"
            return result;
        }

        public " + template + @"(" + typeof(QBuilder).FullName + @" builder): base(builder) {}

" + string.Join(@"
", propertyInfos.Select(_ => "        internal System.Object " + _.Name + ";")) + @"
    }
}
").GetRoot();
                    var descendantNodes = compilationUnitSyntax.ChildNodes().Skip(1).First().DescendantNodes();
                    var syntaxTree = SyntaxTree.Create((CompilationUnitSyntax) new Rewriter(
                        descendantNodes.OfType<MethodDeclarationSyntax>().First().DescendantNodes()
                            .OfType<CastExpressionSyntax>().First(),
                        syntax,
                        descendantNodes.OfType<FieldDeclarationSyntax>().Select(
                            _ => _.DescendantNodes().OfType<VariableDeclarationSyntax>().Single())
                            .Take(typeSyntaxs.Count).Select((declarationSyntax, i) => new {
                                declarationSyntax,
                                typeSyntax = typeSyntaxs[i]
                            }).ToDictionary(_ => _.declarationSyntax, _ => _.typeSyntax)
                        ).Visit(compilationUnitSyntax));
                    var compilation = Compilation.Create(DynamicMaterializer.AssemblyName.Name,
                        new CompilationOptions(OutputKind.DynamicallyLinkedLibrary),
                        new[] {syntaxTree},
                        types.Select(_ => new MetadataFileReference(_.Key.Location,
                            new MetadataReferenceProperties(alias: _.Value)))
                            .Concat(method.DeclaringType.Assembly.GetReferencedAssemblies()
                                .Select(_ => Assembly.Load(_.FullName))
                                .Concat(new[] { method.DeclaringType.Assembly })
                                .Select(_ => new MetadataFileReference(_.Location)))
                        );
                    var className = "Razor." + template;
                    CommonEmitResult emitResult;
                    Func<Type> typeFunc;
                    if (!Debugger.IsAttached)
                    {
                        emitResult = compilation.Emit(DynamicMaterializer.ModuleBuilder);
                        typeFunc = () => DynamicMaterializer.ModuleBuilder.GetType(className);
                    }
                    else
                    {
                        MemoryStream stream;
                        MemoryStream pdbStream;
                        using (stream = new MemoryStream())
                        using (pdbStream = new MemoryStream())
                            emitResult = compilation.Emit(stream, pdbStream: pdbStream);
                        typeFunc = () => Assembly.Load(stream.GetBuffer(), pdbStream.GetBuffer()).GetType(className);
                    }

                    foreach (var diagnostic in emitResult.Diagnostics)
                    {
                        var lineSpan = diagnostic.Location.GetLineSpan(usePreprocessorDirectives: true);
                        throw new ApplicationException(
                            string.Format("Build error: '{3}' (Caller line number {4}, Ln {0}, Col {1}) {2}",
                                lineSpan.StartLinePosition.Line + 1,
                                lineSpan.StartLinePosition.Character + 1,
                                diagnostic.Info.GetMessage(),
                                filePath, lineNumber));
                    }

                    var methodInfo = typeFunc()
                        .GetMethod("M58a3d6d024dc47c6a692e20e29210b89", BindingFlags.Static | BindingFlags.NonPublic);

                    @delegate = (Func<object, QBuilder, TemplateBase>)
                        methodInfo.CreateDelegate(typeof(Func<object, QBuilder, TemplateBase>));
                    razorCache.Set(lineNumber, filePath, new RazorCacheItem(@delegate, sql, type));
                }
                var builder = new QBuilder(commandType);
                @delegate(paramInference, builder).Execute();
                return builder;
            }
            else
            {
                var builder = new QBuilder(commandType);
                builder.Append(sql, (dynamic)ParamInference(param));
                return builder;
            }
        }

        private class RazorEngineHost : System.Web.Razor.RazorEngineHost
        {
            public RazorEngineHost(RazorCodeLanguage codeLanguage)
                : base(codeLanguage)
            {
            }

            public override void PostProcessGeneratedCode(CodeGeneratorContext context)
            {
                base.PostProcessGeneratedCode(context);
                context.GeneratedClass.IsPartial = true;
                context.GeneratedClass.Members.Remove(
                    context.GeneratedClass.Members.OfType<CodeConstructor>().Single());
            }
        }

        private class Rewriter : SyntaxRewriter
        {
            private readonly CastExpressionSyntax castExpressionSyntax;
            private readonly TypeSyntax syntax;
            private readonly Dictionary<VariableDeclarationSyntax, TypeSyntax> dictionary;

            public Rewriter(CastExpressionSyntax castExpressionSyntax, TypeSyntax syntax,
                Dictionary<VariableDeclarationSyntax, TypeSyntax> dictionary)
            {
                this.castExpressionSyntax = castExpressionSyntax;
                this.syntax = syntax;
                this.dictionary = dictionary;
            }

            public override SyntaxNode VisitVariableDeclaration(VariableDeclarationSyntax node)
            {
                TypeSyntax value;
                return dictionary.TryGetValue(node, out value)
                    ? node.WithType(value)
                    : base.VisitVariableDeclaration(node);
            }

            public override SyntaxNode VisitCastExpression(CastExpressionSyntax node)
            {
                return node == castExpressionSyntax
                    ? castExpressionSyntax.WithType(syntax)
                    : base.VisitCastExpression(node);
            }
        }

        private static IRazorConfiguration GetRazorConfiguration(MethodBase method)
        {
            var assembly = method.DeclaringType.Assembly;
            return ((IRazorConfiguration)assembly.CreateInstance(
                assembly.GetTypes().Single(_ => _.GetInterfaces().Contains(typeof(IRazorConfiguration))).FullName));
        }

        private static TypeSyntax GetTypeSyntax(Type type, Dictionary<Assembly, string> dictionary)
        {
            if (type.Namespace != null)
                return NameSyntax(type, dictionary);
            else if (DynamicMaterializer.IsAnonymousType(type))
            {
                string value;
                if (!dictionary.TryGetValue(type.Assembly, out value))
                {
                    value = "r" + dictionary.Count;
                    dictionary.Add(type.Assembly, value);
                }
                return Syntax.AliasQualifiedName(value, SimpleNameSyntax(type, dictionary));
            }
            else
                return SimpleNameSyntax(type, dictionary);
        }

        private static NameSyntax NameSyntax(Type type, Dictionary<Assembly, string> dictionary)
        {
            return Syntax.QualifiedName(
                type.IsNested
                    ? NameSyntax(type.DeclaringType, dictionary)
                    : Syntax.ParseName(type.Namespace),
                SimpleNameSyntax(type, dictionary));
        }

        private static SimpleNameSyntax SimpleNameSyntax(Type type, Dictionary<Assembly, string> dictionary)
        {
            SimpleNameSyntax syntax;
            if (!type.IsGenericType)
                syntax = Syntax.IdentifierName(type.Name);
            else
                syntax = Syntax.GenericName(
                    Syntax.Identifier(type.Name.Substring(0, type.Name.IndexOf("`", StringComparison.Ordinal))),
                    Syntax.TypeArgumentList(
                        Syntax.SeparatedList<TypeSyntax>(
                            WithComma(type.GetGenericArguments().Select(_ => GetTypeSyntax(_, dictionary))).ToArray()
                            )
                        )
                    );
            return syntax;
        }

        private static IEnumerable<SyntaxNodeOrToken> WithComma(IEnumerable<TypeSyntax> enumerable)
        {
            var isFirst = true;
            foreach (var typeSyntax in enumerable)
            {
                if (!isFirst)
                    yield return Syntax.Token(SyntaxKind.CommaToken);
                else
                    isFirst = false;
                yield return typeSyntax;
            }
        }
    }

    public class SubQuery
    {
        internal readonly Action<QWriter> Action;

        public SubQuery(Action<QWriter> action)
        {
            Action = action;
        }
    }

    public class QWriter
    {
        private readonly QBuilder builder;

        public QWriter(QBuilder builder)
        {
            this.builder = builder;
        }

        public void Write<T>(Param<T> arg)
        {
            builder.Append(arg);
        }

        public void Write<T>(Param<T>? arg)
        {
            builder.Append(arg.Value);
        }

        public void Write<T>(Option<Param<T>> arg)
        {
            builder.Append(arg.Value);
        }

        public void Write(SubQuery arg)
        {
            arg.Action(this);
        }

        public void WriteLiteral(string arg)
        {
            builder.Append(arg);
        }

        public void AppendParams<T>(T param)
        {
            builder.Append(param);
        }

        public static void WriteTo<T>(QWriter writer, Param<T> arg)
        {
            writer.Write(arg);
        }

        public static void WriteTo<T>(QWriter writer, Param<T>? arg)
        {
            writer.Write(arg.Value);
        }

        public static void WriteTo<T>(QWriter writer, Option<Param<T>> arg)
        {
            writer.Write(arg.Value);
        }

        public static void WriteTo(QWriter writer, SubQuery arg)
        {
            writer.Write(arg);
        }

        public static void WriteLiteralTo(QWriter writer, string arg)
        {
            writer.WriteLiteral(arg);
        }
    }

    public abstract class TemplateBase
    {
        private readonly QWriter writer;

        protected TemplateBase(QBuilder builder)
        {
            writer = new QWriter(builder);
        }

        public abstract void Execute();

        protected void Write<T>(Param<T> arg)
        {
            QWriter.WriteTo(writer, arg);
        }

        protected void Write<T>(Param<T>? arg)
        {
            QWriter.WriteTo(writer, arg);
        }

        protected void Write<T>(Option<Param<T>> arg)
        {
            QWriter.WriteTo(writer, arg);
        }

        protected void Write(SubQuery arg)
        {
            QWriter.WriteTo(writer, arg);
        }

        protected void WriteLiteral(string arg)
        {
            QWriter.WriteLiteralTo(writer, arg);
        }
    }

    public interface IRazorConfiguration
    {
        IEnumerable<string> GetNamespaceImports(MethodBase method);
    }

    public static class NonQuery
    {
        public static INonQuery New<TParam>(TParam param, string sql, CommandType commandType = CommandType.Text,
            [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string filePath = "")
        {
            return Util.NonQuery(param, sql, commandType, lineNumber, filePath);
        }        
    }

    public static class BatchQuery<T>
    {
        public static IQuery<T> New<TParam>(TParam param, string sql, CommandType commandType = CommandType.Text,
            [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string filePath = "")
        {
            return Util.BatchQuery<T, TParam>(param, sql, commandType, lineNumber, filePath);
        }
    }

    public static class Query<T>
    {
        public static IQuery<Type<T>> New<TParam>(TParam param, string sql, CommandType commandType = CommandType.Text,
            [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string filePath = "")
        {
            return Util.Query<T, TParam>(param, sql, commandType, lineNumber, filePath);
        }
    }

    public interface IQuery
    {
        string CommandText { get; }
        List<Func<SqlParameter>> SqlParameters { get; }
        CommandType CommandType { get; }
    }

// ReSharper disable UnusedTypeParameter
    public interface IQuery<T> : IQuery
// ReSharper restore UnusedTypeParameter
    {
    }

    public interface INonQuery : IQuery
    {
    }

    /// <summary>
    /// //TODO: вытровить
    /// </summary>
    public class ColumnSelector
    {
        internal readonly List<string> ColumnRefs = new List<string>();

        public T _<T>()
        {
            ColumnRefs.Add(string.Empty);
            return default(T);
        }

        public T _<T>(string columnRef)
        {
            ColumnRefs.Add(columnRef);
            return default(T);
        }

        public string String()
        {
            return _<string>();
        }

        public int Int()
        {
            return _<int>();
        }

        public int Int(string columnRef)
        {
            return _<int>(columnRef);
        }

        public DateTime DateTime()
        {
            return _<DateTime>();
        }

    }

    public class QBuilder
    {
        private readonly CommandType commandType;
        internal readonly StringBuilder Builder = new StringBuilder();
        internal readonly List<Func<SqlParameter>> SqlParameters = new List<Func<SqlParameter>>();
        private readonly Dictionary<object, string> @params = new Dictionary<object, string>();

        public QBuilder(CommandType commandType = CommandType.Text)
        {
            this.commandType = commandType;
        }

        public QBuilder(params QItem[] elements) : this()
        {
            Append(elements);
        }

        public CommandType CommandType
        {
            get { return commandType; }
        }

        public QBuilder Append(string value)
        {
            Builder.Append(value);
            return this;
        }

        public QBuilder Append(Action<QBuilder> value)
        {
            value(this);
            return this;
        }

        public QBuilder Append<T>(Param<T> param)
        {
            string name;
            if (!@params.TryGetValue(param, out name))
            {
                name = "@p" + SqlParameters.Count;
                AddParam(name, param);
                @params.Add(param, name);
            }
            return Append(name);
        }

        public QBuilder Append<T>(string value, T param)
        {
            return Append(value).Append(param);
        }

        public QBuilder Append<T>(T param)
        {
            Cache<T>.Action(this, param);
            return this;
        }

        private static class Cache<T>
        {
            public static readonly Action<QBuilder, T> Action = DynamicMaterializer.BuildParamAdder<T>();
        }

        public QBuilder Append(params QItem[] items)
        {
            foreach (var item in items) item.Append(this);
            return this;
        }

        internal void AddParam<T>(string name, Param<T> param)
        {
            SqlParameters.Add(() => {
                var sqlParameter = param.Content.SqlParameterFunc(name);
                //TODO: ProcessValue по перфомансу не очень хорошо
                param.Content.ValueFunc.ProcessValue(_ => sqlParameter.Value = param.Content.ToSqlParameterValueFunc(_()));
                return sqlParameter;
            });
        }

        internal static readonly MethodInfo AddParamMethod =
            GetMethodInfo<Action<QBuilder, string, Param<object>>>((x1, x2, x3) => x1.AddParam(x2, x3))
                .GetGenericMethodDefinition();

        public static MethodInfo GetMethodInfo<T>(Expression<T> expression)
        {
            return ((MethodCallExpression)expression.Body).Method;
        }
    }

    public class QItem
    {
        internal readonly Action<QBuilder> Append;

        internal QItem(Action<QBuilder> append)
        {
            Append = append;
        }

        public static implicit operator QItem(string value)
        {
            return new QItem(builder => builder.Append(value));
        }

        public static implicit operator QItem(Action<QBuilder> value)
        {
            return new QItem(builder => builder.Append(value));
        }
    }

    public static class QueryExtensions
    {
        public static void SetQuery(this SqlCommand command, IQuery query)
        {
            command.CommandType = query.CommandType;
            command.CommandText = query.CommandText;
            foreach (var sqlParameter in query.SqlParameters) command.Parameters.Add(sqlParameter());            
        }

        private static class Cache<T>
        {
            public static readonly List<string> ConstructorParameterNames =
                typeof(T).GetConstructors()[0].GetParameters().Select(_ => _.Name).ToList();
        }

        public static IQuery<Type<T>> Select<T>(this QBuilder it, Func<ColumnSelector, T> columnSelectorFunc)
        {
            if (!DynamicMaterializer.IsAnonymousType(typeof(T))) throw new NotSupportedException();

            var columnSelector = new ColumnSelector();
            columnSelectorFunc(columnSelector);

            var parameterNames = Cache<T>.ConstructorParameterNames;
            var stringBuilder = new StringBuilder();
            for (var index = 0; index < parameterNames.Count; index++)
            {
                if (stringBuilder.Length != 0) stringBuilder.Append(@",
");
                stringBuilder.Append("    " + columnSelector.ColumnRefs[index] + parameterNames[index]);
            }
            it.Builder.Insert(0, stringBuilder.Insert(0, @"SELECT
").AppendLine());
            return it.BatchQuery<Type<T>>();
        }

        public static IQuery<Type<T>> Query<T>(this QBuilder it, T recordExample)
        {
            return it.BatchQuery<Type<T>>();
        }

        public static IQuery<Type<T>> Query<T>(this QBuilder it)
        {
            return it.BatchQuery<Type<T>>();
        }

        /// <summary>
        /// //TODO: убрать. Соответствующего Razor аналога нет.
        /// </summary>
        public static IQuery<Type<T1, T2>> Query<T1, T2>(this QBuilder it)
        {
            return it.BatchQuery<Type<T1, T2>>();
        }

        public static IQuery<T> BatchQuery<T>(this QBuilder it)
        {
            return CreateQuery<T>(it);
        }

        public static Tuple<IQuery<T1>, IQuery<T2>> BatchQuery<T1, T2>(this Tuple<QBuilder, QBuilder> it)
        {
            return Tuple.Create(CreateQuery<T1>(it.Item1), CreateQuery<T2>(it.Item2));
        }

        private static IQuery<T> CreateQuery<T>(QBuilder builder)
        {
            return new QueryImpl<T>(builder.Builder.ToString(), builder.SqlParameters, builder.CommandType);
        }

        private class QueryImpl<T> : IQuery<T>
        {
            private readonly string commandText;
            private readonly List<Func<SqlParameter>> sqlParameters;
            private readonly CommandType commandType;

            public QueryImpl(string commandText, List<Func<SqlParameter>> sqlParameters, CommandType commandType)
            {
                this.commandText = commandText;
                this.sqlParameters = sqlParameters;
                this.commandType = commandType;
            }

            public string CommandText
            {
                get { return commandText; }
            }

            public List<Func<SqlParameter>> SqlParameters
            {
                get { return sqlParameters; }
            }

            public CommandType CommandType
            {
                get { return commandType; }
            }
        }

        public static Type QueryImplementationType
        {
            get { return typeof (QueryImpl<>); }
        }

        public static INonQuery NonQuery(this QBuilder builder)
        {
            return new NonQueryImpl(builder.Builder.ToString(), builder.SqlParameters, builder.CommandType);
        }

        private class NonQueryImpl : INonQuery
        {
            private readonly string commandText;
            private readonly List<Func<SqlParameter>> sqlParameters;
            private readonly CommandType commandType;

            public NonQueryImpl(string commandText, List<Func<SqlParameter>> sqlParameters, CommandType commandType)
            {
                this.commandText = commandText;
                this.sqlParameters = sqlParameters;
                this.commandType = commandType;
            }

            public string CommandText
            {
                get { return commandText; }
            }

            public List<Func<SqlParameter>> SqlParameters
            {
                get { return sqlParameters; }
            }

            public CommandType CommandType
            {
                get { return commandType; }
            }
        }

        public static Type NonQueryImplementationType
        {
            get { return typeof(NonQueryImpl); }
        }
    }
}